package com.gopay.common.cipher.utils;

import com.gopay.common.cipher.constants.SensitiveInfoConstants;
import com.gopay.common.cipher.vo.SecurityInfoKeyBox;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 对data key的一些操作
 * Created by zyt on 2017/4/17.
 */
public class DataKeyUtils {
    private static final Logger logger = LoggerFactory.getLogger(DataKeyUtils.class);

    //配置文件中存放data key的key名字
    public static final String KEY_NAME = "sensitiveinfo.key";
    //redis中存放data key的key名字
    private static String KEY_VALUE;

    //本地内存中的keyInfo备份
    private static SecurityInfoKeyBox securityInfoKeyBox = null;

    //从redis中连续获取失败的最大次数，熔断机制，只访问本地备份
    private static final int MAX_FAIL_NUM = 5;

    //从redis中获取密钥，当前连续失败的次数
    private static volatile AtomicInteger cur_fail_num = new AtomicInteger();

    /**
     * 从redis中获取keyBox
     * @return  redis中的keyBox
     */
    private static SecurityInfoKeyBox getKeyBoxFromRedis(){
        if(KeyLockUtils.getLock()) {
            if(StringUtils.isBlank(KEY_VALUE)){
                KEY_VALUE = getKeyValueFromRedis();
            }
            Object obj = CacheUtils.getObject(KEY_VALUE);
            if (obj instanceof SecurityInfoKeyBox) {
                SecurityInfoKeyBox securityInfoKeyBox = (SecurityInfoKeyBox) obj;
                return securityInfoKeyBox;
            }
        }
        return null;
    }

    /**
     * 设置redis中keyBox的值，仅在redis中keyBox值与当前不一致的时候设置
     * @param securityInfoKeyBox
     */
    public static void setKeyBox(SecurityInfoKeyBox securityInfoKeyBox){
        SecurityInfoKeyBox curSecurityInfoKeyBox = getKeyBoxFromRedis();
        if(needeUpdate(securityInfoKeyBox, curSecurityInfoKeyBox) && hasPermission()) {
            CacheUtils.setObject(KEY_VALUE, securityInfoKeyBox);
        }
    }

    /**
     * 获取当前生效的keyBox，若redis可用，则从redis获取，若redis不可用，或已熔断，则取本地缓存
     * @return
     */
    public static SecurityInfoKeyBox getValidKeyBox(){
        //add by zyt,2017-7-12，项目缺失加解密配置文件的时候，直接试用本地内存
        if(!CacheUtils.checkCacheProp()){
            logger.error("cipher.properties is missed or not correct, this maybe result to an incorrect cipher result" +
                    ", please check properties and restart this server。");
            return securityInfoKeyBox;
        }

        if(cur_fail_num.intValue() >= MAX_FAIL_NUM) {

            logger.error("try to get value from redis reach max times, blow redis connection, use only cache!" );
            return securityInfoKeyBox;
        }
        //modified by zyt, 2017-7-11,临时变量，存储从redis获取的值，优化之前会读两次redis的问题，提高效率
        SecurityInfoKeyBox keyBoxFromRedis;
        if(cur_fail_num.intValue() <= MAX_FAIL_NUM && (keyBoxFromRedis = getKeyBoxFromRedis()) != null){
            //redis获取成功的时候，失败次数归零
            if(cur_fail_num.intValue() != 0){
                cur_fail_num.set(0);
            }
            //判断从redis中获取的密钥信息与当前内存中的备份是否一致，不一致则替换
            if(needeUpdate(keyBoxFromRedis, securityInfoKeyBox)){
                logger.error("检测到密钥更换，同步更新本地缓存...");
                setSecurityInfoKeyBox(keyBoxFromRedis);
            }
            return keyBoxFromRedis;
        }
        if(cur_fail_num.intValue() < MAX_FAIL_NUM) {
            cur_fail_num.incrementAndGet();
            logger.error("Failed to get value from redis, current fail times :" + cur_fail_num);
            //add by zyt ，2017-7-14，访问redis超限的时候，短信通知
            if(cur_fail_num.intValue() >= MAX_FAIL_NUM){
                logger.error("===================redis熔断，发送短信通知");
                SensitiveInfoDictionaryUtils.sendExceptionSms(new Exception("加解密连接redis次数超限，将熔断redis，使用本地缓存。请尽快恢复redis"));
            }
        }
        return securityInfoKeyBox;
    }

    /**
     *
     * @Description: 读取配置文件redis中存放密钥的key值
     * @author zyt
     */
    private static String getKeyValueFromRedis(){
        if(!CacheUtils.isPropFileExist()){
            logger.error("Load cipher.properties failed,perhaps this project don't need cipher function, or missed this properties");
            return null;
        }
        InputStream inputstream = KeyPropertiesUtils.class.getClassLoader().getResourceAsStream(SensitiveInfoConstants.CIPHERPROP_PATH);
        Properties pathConfig = new Properties();
        try {
            pathConfig.load(inputstream);
            String keyValue = pathConfig.getProperty(KEY_NAME);
            return keyValue;
        } catch (IOException e) {
            logger.error("读取MQ配置文件失败:" + e);
            return null;
        }
    }

    /**
     * redis中的密钥信息是否需要重新加载，根据传入的新值于旧值判断
     * @param newSecurityInfoKeyBox 新的密钥信息
     * @return
     */
    private static Boolean needeUpdate(SecurityInfoKeyBox newSecurityInfoKeyBox, SecurityInfoKeyBox oldSecurityInfoKeyBox){
        if(newSecurityInfoKeyBox == null || newSecurityInfoKeyBox.getCurrentKeyInfo() == null
                || StringUtils.isBlank(newSecurityInfoKeyBox.getCurrentKeyInfo().getKeyContent())){
            return false;
        }
        if(oldSecurityInfoKeyBox == null || oldSecurityInfoKeyBox.getCurrentKeyInfo() == null
                || StringUtils.isBlank(oldSecurityInfoKeyBox.getCurrentKeyInfo().getKeyContent())){
            return true;
        }
        if(!StringUtils.equals(newSecurityInfoKeyBox.getCurrentKeyInfo().getKeyContent(), oldSecurityInfoKeyBox.getCurrentKeyInfo().getKeyContent())){
            return true;
        }
        if(newSecurityInfoKeyBox.getAllKeyInfos().size() != oldSecurityInfoKeyBox.getAllKeyInfos().size()){
            return true;
        }
        return false;
    }

    /**
     *
     * @Description: 	(保证该只有SecurityKeysSupervise接口有修改权限
     * @return 			是SecurityKeysSupervise调用返回true，否则返回false
     * @author zyt
     */
    private static boolean hasPermission(){
        StackTraceElement[] stackElements = Thread.currentThread().getStackTrace();
        if (stackElements != null) {
            for (StackTraceElement ste : stackElements) {
                if(StringUtils.containsIgnoreCase(ste.getClassName(), "SecurityKeysSupervise")){
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 设置缓存中的keyBox
     * @param securityInfoKeyBox
     */
    public static void setSecurityInfoKeyBox(SecurityInfoKeyBox securityInfoKeyBox) {
        StackTraceElement[] stackElements = Thread.currentThread().getStackTrace();
        if (stackElements != null) {
            for (StackTraceElement ste : stackElements) {
                if(StringUtils.containsIgnoreCase(ste.getClassName(), "SecurityKeysSupervise")
                        || StringUtils.containsIgnoreCase(ste.getClassName(), "DataKeyUtils")){
                    if(DataKeyUtils.securityInfoKeyBox == null
                            || DataKeyUtils.securityInfoKeyBox.getAllKeyInfos().size() == 0){
                        DataKeyUtils.securityInfoKeyBox = new SecurityInfoKeyBox();
                        DataKeyUtils.securityInfoKeyBox.setCurrentKeyInfo(securityInfoKeyBox.getCurrentKeyInfo());
                        DataKeyUtils.securityInfoKeyBox.setAllKeyInfos(securityInfoKeyBox.getAllKeyInfos());
                    } else {
                        DataKeyUtils.securityInfoKeyBox.setCurrentKeyInfo(securityInfoKeyBox.getCurrentKeyInfo());
                        DataKeyUtils.securityInfoKeyBox.setAllKeyInfos(securityInfoKeyBox.getAllKeyInfos());
                    }
                    break;
                }
            }
        }
    }
}
